/*
This file is part of IRCaBot.
IRCaBot is IRC logger with features.
Source code: https://notabug.org/acetone/ircabot.
Copyright (C) acetone, 2023.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "voicedlist.h"
#include "currenttime.h"
#include "config.h"
#include "fsconstants.h"

#include <QDir>
#include <QFile>
#include <QDebug>

constexpr const int SERIALIZE_MINIMAL_INTERVAL = 120;

VoicedList::VoicedList(const QString &networkName) : m_lastSerializeTimestamp(currentTimestampSecs()-SERIALIZE_MINIMAL_INTERVAL)
{
    setNetworkName(networkName);
}

VoicedList::VoicedList() : m_lastSerializeTimestamp(currentTimestampSecs()-SERIALIZE_MINIMAL_INTERVAL)
{

}

void VoicedList::setNetworkName(const QString &networkName)
{
    m_networkName = networkName.toLower();
    deserialize();
}

void VoicedList::updateActivity(const QString &nickname)
{
    QMutexLocker lock (&m_timestampsMtx);
    if (m_timestamps.find(nickname.toLower()) == m_timestamps.end()) return;

    m_timestamps[nickname.toLower()] = currentTimestampSecs();
    lock.unlock();

    serialize();
}

void VoicedList::add(const QString &nickname)
{
    QMutexLocker lock (&m_timestampsMtx);

    m_timestamps[nickname.toLower()] = currentTimestampSecs();
    lock.unlock();

    serialize();
}

bool VoicedList::del(const QString &nickname)
{
    QMutexLocker lock (&m_timestampsMtx);

    auto iter = m_timestamps.find(nickname.toLower());
    if (iter != m_timestamps.end())
    {
        m_timestamps.erase(iter);
    }
    else
    {
        return false;
    }
    lock.unlock();

    serialize();
    return true;
}

QStringList VoicedList::nicks()
{
    QMutexLocker lock (&m_timestampsMtx);

    QStringList result, toRemove;

    const auto now = currentTimestampSecs();
    QHashIterator<QString, qint64> timestampsIter(m_timestamps);
    while (timestampsIter.hasNext())
    {
        auto nick = timestampsIter.next();
        if (now - nick.value() > VOICEFLAG_INTERVAL_TO_REMOVE_FROM_DATABASE)
        {
            toRemove.push_back(nick.key());
        }
        else if (now - nick.value() < VOICEFLAG_TIMEOUT)
        {
            result.push_back(nick.key());
        }
    }

    QStringListIterator toRemoveIter(toRemove);
    while (toRemoveIter.hasNext())
    {
        m_timestamps.remove(toRemoveIter.next());
    }

    if (not toRemove.isEmpty())
    {
        lock.unlock();
        serialize();
    }

    return result;
}

QMap<QString, qint64> VoicedList::nicksWithTimestamps()
{
    QMutexLocker lock (&m_timestampsMtx);

    QStringList toRemove;
    QMap<QString, qint64> result;

    const auto now = currentTimestampSecs();
    QHashIterator<QString, qint64> timestampsIter(m_timestamps);
    while (timestampsIter.hasNext())
    {
        auto nick = timestampsIter.next();
        if (now - nick.value() > VOICEFLAG_INTERVAL_TO_REMOVE_FROM_DATABASE)
        {
            toRemove.push_back(nick.key());
        }
        else if (now - nick.value() < VOICEFLAG_TIMEOUT)
        {
            result[nick.key()] = nick.value();
        }
    }

    QStringListIterator toRemoveIter(toRemove);
    while (toRemoveIter.hasNext())
    {
        m_timestamps.remove(toRemoveIter.next());
    }

    if (not toRemove.isEmpty())
    {
        lock.unlock();
        serialize();
    }

    return result;
}

void VoicedList::serialize()
{
    const auto now = currentTimestampSecs();
    if (now - m_lastSerializeTimestamp < SERIALIZE_MINIMAL_INTERVAL) return;

    m_lastSerializeTimestamp = now;

    if (not QDir(dir()).exists())
    {
        if (not QDir().mkpath(dir()))
        {
            qCritical() << "VoicedList::serialize failed: can't create dir:" << dir();
            return;
        }
    }

    QFile file(path());
    if (not file.open(QIODevice::WriteOnly))
    {
        qCritical() << "VoicedList::serialize failed: can't write file:" << path();
        return;
    }

    QString stringiedData;

    auto nicks = nicksWithTimestamps();
    QMapIterator<QString, qint64> nicksIter(nicks);
    while (nicksIter.hasNext())
    {
        auto nick = nicksIter.next();
        stringiedData += nick.key() + ":" + QString::number(nick.value());

        if (nicksIter.hasNext()) stringiedData += "\n";
    }

    QByteArray binaryData = stringiedData.toUtf8();
    auto written = file.write(binaryData);
    if (written != binaryData.size())
    {
        qCritical() << "VoicedList::serialize failed: Some of the data was lost. Written: " << written << "/" << binaryData.size() << "bytes";
    }
    file.close();
}

void VoicedList::deserialize()
{
    QFile file( path() );
    if (not file.exists()) return;

    if (not file.open(QIODevice::ReadOnly))
    {
        qCritical() << "VoicedList::deserialize failed: can't read file:" << path();
        return;
    }
    QString raw = file.readAll();
    file.close();

    QStringList lines = raw.split('\n');
    QStringListIterator linesIter(lines);
    while (linesIter.hasNext())
    {
        QStringList line = linesIter.next().split(':');
        if (line.size() != 2)
        {
            qWarning().noquote() << "VoicedList::deserialize anomaly: invalid line:" << line;
            continue;
        }
        QString nickname = line.at(0);
        if (nickname.isEmpty())
        {
            qWarning().noquote() << "VoicedList::deserialize anomaly: invalid line (nickname is empty):" << line;
            continue;
        }
        qint64 timestamp = line.at(1).toLongLong();
        if (timestamp == 0)
        {
            qWarning().noquote() << "VoicedList::deserialize anomaly: invalid line (timestamp):" << line;
            continue;
        }

        QMutexLocker lock (&m_timestampsMtx);
        m_timestamps[nickname] = timestamp;
    }
}

QString VoicedList::path()
{
    if (m_networkName.isEmpty())
    {
        qCritical() << "VoicedList::path called before setNetworkName()";
    }
    if (m_path.isEmpty())
    {
        m_path = Config::workingDirectory() + QString(FS_NETWORKS_DIR) + FS_NATIVE_SEPARATOR + m_networkName + FS_NATIVE_SEPARATOR + QString(FS_VOICEGATE_CACHE);
    }

    return m_path;
}

QString VoicedList::dir()
{
    if (m_networkName.isEmpty())
    {
        qCritical() << "VoicedList::dir called before setNetworkName()";
    }

    if (m_dir.isEmpty())
    {
        m_dir = Config::workingDirectory() + QString(FS_NETWORKS_DIR) + FS_NATIVE_SEPARATOR + m_networkName;
    }

    return m_dir;
}
